/*** Created Time: 2022年5月12日 20:58:04 ***/
/*** Author: Eric                        ***/

#include <onix/console.h>
#include <onix/io.h>
#include <onix/string.h>
#include <onix/interrupt.h>

// 关键数据的宏定义
#define     CRT_ADDR_REG        (0x3D4)   // CRT(6845芯片)索引寄存器/地址寄存器
#define     CRT_DATA_REG        (0x3D5)   // CRT(6845芯片)数据寄存器

#define     CRT_START_ADDR_H    (0xC)     // 显示内存起始位置 - 高位
#define     CRT_START_ADDR_L    (0xD)     // 显示内存起始位置 - 低位
#define     CRT_CURSOR_H        (0xE)     // 光标位置 - 高位
#define     CRT_CURSOR_L        (0xF)     // 光标位置 - 低位

#define     MEM_BASE            (0xB8000)               // 显卡内存起始位置
#define     MEM_SIZE            (0x4000)                // 显卡内存大小，16k
#define     MEM_END             (MEM_BASE + MEM_SIZE)   // 显卡内存结束位置
#define     WIDTH               (80)                    // 屏幕文本列数
#define     HEIGHT              (25)                    // 屏幕文本行数
#define     ROW_SIZE            (WIDTH * 2)             // 每行字节数
#define     SCR_SIZE            (ROW_SIZE * HEIGHT)     // 屏幕字节数

#define     ASCII_NUL           (0x00)
#define     ASCII_ENQ           (0x05)      // 传送应答消息
#define     ASCII_BEL           (0x07)      // \a 从键盘发出声响
#define     ASCII_BS            (0x08)      // \b 将光标移向左边一个字符
#define     ASCII_HT            (0x09)      // \t 制表符
#define     ASCII_LF            (0x0A)      // \n 换行
#define     ASCII_VT            (0x0B)      // \v 作用如LF
#define     ASCII_FF            (0x0C)      // \f 作用如LF
#define     ASCII_CR            (0x0D)      // \r 回车
#define     ASCII_DEL           (0x7F)      // 在输入时忽略，不保存在输入缓冲中


// 当前显示器文件所实现的辅助函数声明
static void get_screen(void);
static void set_screen(void);
static void get_cursor(void);
static void set_cursor(void);
static void scroll_up(void);
static void commmand_lf(void);
static void commmand_cr(void);
static void commmand_bs(void);
static void commmand_del(void);


extern void start_beep(void);   // clock.c 蜂鸣器函数


// 静态全局变量声明
static u32 screen;              // 当前显示器开始的内存位置
static u32 pos;                 // 记录当前光标的内存位置
static u32 x, y;                // 记录当前光标的坐标

static u8 attr = 7;             // 字符样式

static u16 erase = 0x0720;      // 空格，高字节07是样式，低字节20是空格的ASCII码


// 辅助函数，获得当前显示器的开始位置
static void get_screen(void)
{
    outb(CRT_ADDR_REG, CRT_START_ADDR_H);   // 开始位置的高地址
    screen = inb(CRT_DATA_REG) << 8;        // 开始位置的高8位
    outb(CRT_ADDR_REG, CRT_START_ADDR_L);   // 开始位置的低地址
    screen |= inb(CRT_DATA_REG);            // 组合获得开始位置

    screen <<= 1;   // screen *= 2
    screen += MEM_BASE; // 加上基址
}

// 辅助函数，设置当前显示器的开始位置
static void set_screen(void)
{
    outb(CRT_ADDR_REG, CRT_START_ADDR_H);   // 开始位置的高地址
    outb(CRT_DATA_REG, ((screen-MEM_BASE) >> 9) & 0xff);
    outb(CRT_ADDR_REG, CRT_START_ADDR_L);   // 开始位置的低地址
    outb(CRT_DATA_REG, ((screen-MEM_BASE) >> 1) & 0xff);
}

// 辅助函数，获得当前光标位置
static void get_cursor(void)
{
    outb(CRT_ADDR_REG, CRT_CURSOR_H);       // 开始位置的高地址
    screen = inb(CRT_DATA_REG) << 8;        // 开始位置的高8位
    outb(CRT_ADDR_REG, CRT_CURSOR_L);       // 开始位置的低地址
    screen |= inb(CRT_DATA_REG);            // 组合获得开始位置

    get_screen();   // 获得当前显示器的开始位置

    pos <<= 1;      // pos *= 2
    pos += MEM_BASE;

    u32 delta = (pos - screen) >> 1;    // 获取光标的相对内存位置
    x = delta % WIDTH;
    y = delta / WIDTH;
}

// 辅助函数，设置当前光标位置
static void set_cursor(void)
{
    outb(CRT_ADDR_REG, CRT_CURSOR_H);   // 光标的高地址
    outb(CRT_DATA_REG, ((pos-MEM_BASE) >> 9) & 0xff);
    outb(CRT_ADDR_REG, CRT_CURSOR_L);   // 光标的低地址
    outb(CRT_DATA_REG, ((pos-MEM_BASE) >> 1) & 0xff);
}

// 清屏函数
void console_clear(void)
{
    screen = MEM_BASE;  // 设置显示器开始位置为内存起始位置
    pos = MEM_BASE;     // 设置当前光标的内存位置为内存起始位置
    x = y = 0;          // 光标绝对坐标设置为(0, 0)
    set_cursor();
    set_screen();

    u16 *ptr = (u16 *)MEM_BASE;
    while(ptr < (u16 *)MEM_END) {
        *ptr++ = erase; // 从内存起始位置到结束位置全部设置为空格，即清屏
    }
}

// 辅助函数，向上滚屏
static void scroll_up(void)
{
    if(screen + SCR_SIZE + ROW_SIZE >= MEM_END) {
        // 如果屏幕字符超过了最大显示数
        memcpy((void *)MEM_BASE, (void *)screen, SCR_SIZE);
        pos -= (screen - MEM_BASE); // 复位
        screen = MEM_BASE;          // 复位
    }

    // 将屏幕最下方一行清空
    u32 *ptr = (u32 *)(screen + SCR_SIZE);
    for(size_t i=0; i<WIDTH; i++) {
        *ptr++ = erase;
    }
    screen += ROW_SIZE;
    pos += ROW_SIZE;
    
    set_screen();
}

// 辅助函数，换行符
static void commmand_lf(void)
{
    if(y+1 < HEIGHT) {  // 没超过最大行数
        y++;
        pos += ROW_SIZE;
        return;
    }

    scroll_up();    // 超过最大函数时滚动屏幕
}

// 辅助函数，回车符
static void commmand_cr(void)
{
    pos -= (x << 1);
    x = 0;
}

// 辅助函数，向左删除一个字符
static void commmand_bs(void)
{
    if(x) {
        x--;
        pos -= 2;
        *(u16 *)pos = erase;
    }
}

// 辅助函数，删除当前字符
static void commmand_del(void)
{
    *(u16 *)pos = erase;    // 删除当前字符
}

// 功能：向显示器写字符并输出
// @buf : 输入字符串
// @count : 输入字符串长度
int32 console_write(char *buf, u32 count)
{
    // 禁止中断，该console_write函数作为临界区(这是未引入互斥量时的解决方案)
    bool intr = interrupt_disable(); 

    char ch;    //当前输出字符
    int32 nr = 0;
    while(nr++ < count) {
        ch = *buf++;

        switch (ch)
        {
        case ASCII_NUL:
            break;
        case ASCII_BEL:
            start_beep();   // \a，发音
            break;
        case ASCII_BS:
            commmand_bs();
            break;
        case ASCII_HT:
            break;
        case ASCII_LF:  // 组合换行+回车
            commmand_lf();
            commmand_cr();
            break;
        case ASCII_VT:
            break;
        case ASCII_FF:
            commmand_lf();
            break;
        case ASCII_CR:  // 回车
            commmand_cr();
            break;
        case ASCII_DEL:
            commmand_del();
            break;
        default:
            if(x >= WIDTH) {    // 当x大于最大宽度
                x -= WIDTH;
                pos -= ROW_SIZE;
                commmand_lf();  // 向下移动一行，即换行
            }

            *((char *)pos) = ch;
            pos++;
            *((char *)pos) = attr;
            pos++;
            x++;

            break;
        }
    }

    set_cursor();

    // 恢复中断
    set_interrupt_state(intr);

    return nr;  // 返回写入字符的数量
}

// 显示器初始化函数
void console_init()
{
    console_clear();
}
